<?php
/*
 * Copyright 2010 Johannes M. Schmitt
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Allows to compile javascript files with Google's Closure Compiler
 * 
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class GoogleClosureCompiler extends JavaCompiler
{
  const C_LEVEL_WHITESPACE_ONLY = 'WHITESPACE_ONLY';
  const C_LEVEL_SIMPLE_OPTIMIZATIONS = 'SIMPLE_OPTIMIZATIONS';
  const C_LEVEL_ADVANCED_OPTIMIZATIONS = 'ADVANCED_OPTIMIZATIONS';
  
  const FORMATTING_PRETTY_PRINT = 'PRETTY_PRINT';
  const FORMATTING_PRINT_INPUT_DELIMITER = 'PRINT_INPUT_DELIMITER';
  
  const WARNING_LEVEL_QUIET = 'QUIET';
  const WARNING_LEVEL_DEFAULT = 'DEFAULT';
  const WARNING_LEVEL_VERBOSE = 'VERBOSE';
  
  const SUMMARY_DETAIL_LEVEL_NEVER = 0;
  const SUMMARY_DETAIL_LEVEL_ERRORS_AND_WARNINGS = 1;
  const SUMMARY_DETAIL_LEVEL_IF_TYPE_CHECKING = 2;
  const SUMMARY_DETAIL_LEVEL_ALWAYS = 3;
  
  const DEV_MODE_OFF = 'OFF';
  const DEV_MODE_START = 'START';
  const DEV_MODE_START_AND_END = 'START_AND_END';
  const DEV_MODE_EVERY_PASS = 'EVERY_PASS';
  
  /**
   * The javascript code to compile
   * @var string
   */
  protected $jsCode;
  protected $jsFiles;
  protected $compilationLevel;
  
  /**
   * The externals code to use.
   * @var string 
   */
  protected $externsCode; 
  protected $excludeDefaultExterns = false;
  protected $formatting;
  protected $warningLevel = self::WARNING_LEVEL_VERBOSE;
  protected $sourceMap;
  protected $variableMapInput;
  protected $variableMapInputFile;
  protected $summaryDetailLevel;
  
  protected $compiled = false;
  protected $compiledCode;
  
  public function __construct($jsCode, $compilationLevel = null)
  {
    if (is_string($jsCode))
      $this->jsCode = $jsCode;
    else if (is_array($jsCode))
    {
      $this->jsFiles = $jsCode;
    	
      // if there are more than 10 files, we need to combine them into one
      // since there is a maximum number of chars a single command line can have;
      // currently, 10 is arbirtrary chosen, and might need adjustment
      if (count($this->jsFiles) > 1000)
      {
      	$this->jsCode = '';
      	foreach ($this->jsFiles as $file)
      		$this->jsCode .= file_get_contents($file)."\n\n";
      	
      	$this->jsFiles = null;
      }
    }
    else
      throw new InvalidArgumentException('$jsCode must either be the code itself, or an array of js files.');
    
    if ($compilationLevel === null)
      $compilationLevel = self::C_LEVEL_SIMPLE_OPTIMIZATIONS;
      
    if (!in_array($compilationLevel, array(self::C_LEVEL_ADVANCED_OPTIMIZATIONS, self::C_LEVEL_SIMPLE_OPTIMIZATIONS, self::C_LEVEL_WHITESPACE_ONLY), true))
      throw new InvalidArgumentException('The compilation level is invalid.');

    $this->compilationLevel = $compilationLevel;
    
    parent::__construct();
  }
  
  protected function setup()
  {
    // input charset for all files
    $this->addCommandLineFlag('charset');
    
    // Specifies the compilation level to use.
    $this->addCommandLineFlag('compilation_level');

    // Runs the compile job many times, then prints out the best phase ordering from this run
    $this->addCommandLineFlag('compute_phase_ordering');
    
    // If true, variable renaming and property renaming map files will be
    // produced as {binary name}_vars_map.out and {binary name}_props_map.out. Note
    // that this flag cannot be used in conjunction with either 
    // variable_map_output_file or property_map_output_file
    $this->addCommandLineFlag('create_name_map_files');
    
    // if specified, a source map file mapping the generated source files
    // back to the original source file will be output to the specified path.
    // The %outname% placeholder will expand to the name of the output file that
    // he source map corresponds to
    $this->addCommandLineFlag('create_source_map');
    
    // enable debugging options
    $this->addCommandLineFlag('debug');
    
    // Override the value of a varaible annotated @define. The format is
    // <name>[=<val>], where <name> is the name of a @define variable and
    // <val> is a boolean, number, or a single-quoted string that contains
    // no single quotes. If [=<val>] is omitted the variable is marked true.
    $this->addCommandLineFlag('define');
    
    // The file containing javascript externs. You may specifiy multiple.
    $this->addCommandLineFlag('externs');
    
    // Specifies which formatting options, if any, should be applied to the output JS.
    $this->addCommandLineFlag('formatting');
    
    // The javascript filename. You may specify multiple.
    $this->addCommandLineFlag('js');
    
    // Primary output filename. If not specified, output is written to stdout
    $this->addCommandLineFlag('js_output_file');
    
    // Turns on extra sanity checks
    $this->addCommandLineFlag('jscomp_dev_mode');
    
    // Make the named class of warnings an error. Options:accessControls, checkVars,
    // checkTypes, deprecated, fileoverviewTags, invalidCasts, missingProperties,
    // nonStandardJsDocs, strictModuleDepCheck, undefinedVars, unknownDefines, visibility
    $this->addCommandLineFlag('jscomp_error');
    
    // Turn off the named class of warnings.  Options:accessControls, checkVars,
    // checkTypes, deprecated, fileoverviewTags, invalidCasts, missingProperties,
    // nonStandardJsDocs, strictModuleDepCheck, undefinedVars, unknownDefines, visibility
    $this->addCommandLineFlag('jscomp_off');
    
    // Make the named class of warnings a normal warning. Options:accessControls, checkVars,
    // checkTypes, deprecated, fileoverviewTags, invalidCasts, missingProperties,
    // nonStandardJsDocs, strictModuleDepCheck, undefinedVars, unknownDefines, visibility
    $this->addCommandLineFlag('jscomp_warning');
    
    // The logging level (standard java.util.logging.Level values) for Compiler
    // progress. Does not control errors or warnings for the JavaScript code
        // under compilation
    $this->addCommandLineFlag('logging_level');
    
    // A javascript module specification. The format is <name>:<num-js-files>[:[<dep>,...][:]]].
    // The module names must be unique. Each dep is the name a module that this module depends
    // on. Modules must be listed in dependency order, and js source files must be listed
    // in the corresponding order. Where --module flags occur in relation to --js flags is
    // unimportant.
    $this->addCommandLineFlag('module');
    
    // Prefix for the filenames of compiled js module. <module-name>.js will be appended to
    // this prefix. Directories will be created as needed. Use with --module; default: ./
    $this->addCommandLineFlag('module_output_path_prefix');
    
    // An output wrapper for a javascript module (optional). The format is <name>:<wrapper>.
    // The module name must correspond witha module specified using --module. The wrapper 
    // must contain %s as the code placeholder
    $this->addCommandLineFlag('module_wrapper');
    
    // Interpolate output into this string at the place denoted by the marker token %output%.
    // See --output_wrapper_marker
    $this->addCommandLineFlag('output_wrapper');
    
    // use this token as output marker in the value of --output_wrapper; default: %output%
    $this->addCommandLineFlag('output_wrapper_marker');
    
    // Prints a dot file describing the internal abstract syntax tree and exits
    $this->addCommandLineFlag('print_ast');
    
    // Prints a dot file describing the passes that will get run and exits
    $this->addCommandLineFlag('print_pass_graph');
    
    // Prints out the parse tree and exits
    $this->addCommandLineFlag('print_tree');
    
    // Process built-ins from the Closure library, such as goog.require(), goog.provide(),
    // And goog.exportSymbol(); default: true
    $this->addCommandLineFlag('process_closure_primitives');
    
    // File containing the serialized version of the property renaming map 
        // produced by a previous compilation
    $this->addCommandLineFlag('property_map_input_file');
    
    // File where the serialized version of the property renaming map produced should be saved.
    $this->addCommandLineFlag('property_map_output_file');
    
    // Controls how detailed the compilation summary is. Values: 0 <never print summary>, 
    // 1 <print summary only if there are errors or warnings>, 2 <print summary if type
    // checking is on, see --check_types>, 3 <always print summary). The default is 1
    $this->addCommandLineFlag('summary_detail_level');
    
    // Check source validity but do not enforce closure style rules and conventions; 
    // default: false
    $this->addCommandLineFlag('third_party');
    
    // specifies whether the default externs should be excluded; default: false
    $this->addCommandLineFlag('use_only_custom_externs');
    
    // File containing the serialized version of the variable renaming map produced by a
    // previous compilation.
    $this->addCommandLineFlag('variable_map_input_file');
    
    // File where the serialized version of the variable renaming map 
    // produced should be saved
    $this->addCommandLineFlag('variable_map_output_file');
    
    // Specifies the warning level to use.
    $this->addCommandLineFlag('warning_level');
  }
  
  /**
   * Sets the compilation level that should be used
   * @param integer $level One of the C_LEVEL_* constants
   */
  public final function setCompilationLevel($level)
  {
  	$this->compilationLevel = $level;
  }
  
  public final function getCompilationLevel()
  {
  	return $this->compilationLevel;
  }
  
  public function setFormatting($formatting)
  {
    $this->formatting = $formatting;
    return $this;
  }
  
  public function setExternsCode($resource)
  {
    $this->externsCode = $resource;
    return $this;
  }
  
  public function useClosureLibrary($status = true)
  {
    $this->useClosureLibrary = $status === true;
    return $this;
  }
  
  public function excludeDefaultExterns($status = true)
  {
    $this->excludeDefaultExterns = $status === true;
    return $this;
  }
  
  public function getJarPath()
  {
    return dirname(dirname(dirname(__FILE__))).
    				'/data/closure_compiler/compiler.jar';
  }
  
  public function setVariableMapInput($input)
  {
    $this->variableMapInput = $input;
    return $this;
  }
  
  public function getVariableMapInput()
  {
    return $this->variableMapInput;
  }
  
  public function setVariableMapInputFile($file)
  {
    $this->variableMapInputFile = $file;
    return $this;
  }
  
  public function getVariableMapInputFile()
  {
    return $this->variableMapInputFile;
  }
  
  public function getSourceMap()
  {
    return $this->sourceMap;
  }
  
  public function setStrictestDebugging()
  {
  	$this->setCommandLineFlag('debug', true);
  	$this->setCommandLineFlag('jscomp_dev_mode', self::DEV_MODE_EVERY_PASS);
  	$this->setCommandLineFlag('jscomp_error', array(
  		'accessControls', 'checkRegExp', 'checkTypes', 'checkVars', 'deprecated', 'fileoverviewTags', 
  		'invalidCasts', 'missingProperties', 'nonStandardJsDocs', 'strictModuleDepCheck', 'undefinedVars', 
  		'unknownDefines', 'visibility',
  	));
  }
  
  public function execute()
  {
    if ($this->executed)
      return;
    
    $tempDir = sys_get_temp_dir();      
      
    // set some command line flags
    $this->setCommandLineFlag('charset', 'utf-8');
    $this->setCommandLineFlag('compilation_level', $this->compilationLevel);
    
    foreach ((array) $this->formatting as $fOpt)
    	$this->setCommandLineFlag('formatting', $fOpt);
    
    // set some file paths
    $outputFile = tempnam($tempDir, 'output');
    $sourceMapFile = tempnam($tempDir, 'sourceMap');
    $this->setCommandLineFlag('create_source_map', $sourceMapFile);
    $this->setCommandLineFlag('js_output_file', $outputFile);
    
    // add input
    if ($this->jsCode !== null)
    {
      $inputFile = tempnam($tempDir, 'input');
      file_put_contents($inputFile, $this->jsCode);
      $this->setCommandLineFlag('js', $inputFile);
    }
    else
      $this->setCommandLineFlag('js', $this->jsFiles);
    
    // add externs
    if ($this->externsCode !== null)
    {
      $externsFile = tempnam($tempDir, 'externs');
      file_put_contents($externsFile, $this->externsCode);
      $this->setCommandLineFlag('externs', $externsFile);
    }
    
    // add variable map
    if ($this->variableMapInput !== null && $this->variableMapInputFile !== null)
      throw new RuntimeException('You can either specify a file, or the serialized string; not both.');
    else if ($this->variableMapInput !== null)
    {
      $variableInputFile = tempnam($tempDir, 'variableInput');
      file_put_contents($variableInputFile, $this->variableMapInput);
      $this->setCommandLineFlag('variable_map_input_file', $variableInputFile);
    }
    else if ($this->variableMapInputFile !== null)
      $this->setCommandLineFlag('variable_map_input_file', $this->variableMapInputFile);
      
    parent::execute();
    
    $this->compiledCode = file_get_contents($outputFile);
    unlink($outputFile);
    
    // check that the output wrapper got applied correctly
    if ($this->hasCommandLineFlag('output_wrapper'))
    {
    	$wrapper = end($this->getCommandLineFlag('output_wrapper'));
    	$wrapperMarker = $this->hasCommandLineFlag('output_wrapper_marker')?
    		end($this->getCommandLineFlag('output_wrapper_marker')) : '%output%';
    		
    	if (($pos = strpos($wrapper, $wrapperMarker)) === false)
    		throw new RuntimeException('The output wrapper does not contain the wrapper marker.');
    		
    	// on windows there the output_wrapper seems not always to be applied,
    	// so we do this manually
    	if (strpos($this->compiledCode, substr($wrapper, 0, $pos)) !== 0)
    		$this->compiledCode = str_replace($wrapperMarker, $this->compiledCode, $wrapper);
    }
    
    $this->sourceMap = file_get_contents($sourceMapFile);
    unlink($sourceMapFile);
    if (isset($inputFile))
      unlink($inputFile);
    if (isset($externsFile))
      unlink($externsFile);
  }
    
  public function getCompiledCode()
  {
    $this->execute();
    return $this->compiledCode;
  }
  
  public final function addDefine($name, $value)
  {
  	if (!is_string($value) && !is_bool($value) && !is_numeric($value))
  		throw new InvalidArgumentException('$value can be a string, boolean, or number.');
  	
  	if (is_string($value))
  		$value = "'".$value."'";
  	else if (is_bool($value))
  		$value = $value? 'true' : 'false';	
  		
  	$this->setCommandLineFlag('define', sprintf('%s=%s', $name, $value));
  }
}